/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2008-2009 Sun Microsystems, Inc.
 * Portions Copyright 2011-2016 ForgeRock AS.
 */
package org.opends.server.extensions;

import static org.opends.messages.ExtensionMessages.*;
import static org.opends.server.util.ServerConstants.*;
import static org.opends.server.util.StaticUtils.*;

import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.HashMap;
import java.util.List;

import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.LoginContext;
import javax.security.sasl.AuthorizeCallback;
import javax.security.sasl.RealmCallback;
import javax.security.sasl.Sasl;
import javax.security.sasl.SaslException;
import javax.security.sasl.SaslServer;

import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.LocalizedIllegalArgumentException;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.reactive.LDAPClientConnection2;
import org.ietf.jgss.GSSException;
import org.opends.server.api.AuthenticationPolicyState;
import org.opends.server.api.ClientConnection;
import org.opends.server.api.IdentityMapper;
import org.opends.server.core.AccessControlConfigManager;
import org.opends.server.core.BindOperation;
import org.opends.server.core.DirectoryServer;
import org.opends.server.core.PasswordPolicyState;
import org.opends.server.protocols.internal.InternalClientConnection;
import org.opends.server.protocols.ldap.LDAPClientConnection;
import org.opends.server.types.AuthenticationInfo;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.Entry;
import org.opends.server.types.Privilege;

/**
 * This class defines the SASL context needed to process GSSAPI and DIGEST-MD5
 * bind requests from clients.
 */
public class SASLContext implements CallbackHandler,
    PrivilegedExceptionAction<Boolean>
{
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();



  /**
   * Instantiate a GSSAPI/DIGEST-MD5 SASL context using the specified
   * parameters.
   *
   * @param saslProps
   *          The properties to use in creating the SASL server.
   * @param serverFQDN
   *          The fully qualified domain name to use in creating the SASL
   *          server.
   * @param mechanism
   *          The SASL mechanism name.
   * @param identityMapper
   *          The identity mapper to use in mapping identities.
   * @return A fully instantiated SASL context to use in processing a SASL bind
   *         for the GSSAPI or DIGEST-MD5 mechanisms.
   * @throws SaslException
   *           If the SASL server can not be instantiated.
   */
  public static SASLContext createSASLContext(
      final HashMap<String, String> saslProps, final String serverFQDN,
      final String mechanism, final IdentityMapper<?> identityMapper)
      throws SaslException
  {
    return new SASLContext(saslProps, serverFQDN, mechanism, identityMapper);
  }



  /** The SASL server to use in the authentication. */
  private SaslServer saslServer;

  /** The identity mapper to use when mapping identities. */
  private final IdentityMapper<?> identityMapper;

  /** The property set to use when creating the SASL server. */
  private final HashMap<String, String> saslProps;

  /** The fully qualified domain name to use when creating the SASL server. */
  private final String serverFQDN;

  /** The SASL mechanism name. */
  private final String mechanism;

  /** The authorization entry used in the authentication. */
  private Entry authEntry;

  /** The authorization entry used in the authentication. */
  private Entry authzEntry;

  /** The user name used in the authentication taken from the name callback. */
  private String userName;

  /** Error message used by callbacks. */
  private LocalizableMessage cbMsg;

  /** Error code used by callbacks. */
  private ResultCode cbResultCode;

  /** The current bind operation used by the callbacks. */
  private BindOperation bindOp;

  /** Used to check if negotiated QOP is confidentiality or integrity. */
  private static final String confidentiality = "auth-conf";
  private static final String integrity = "auth-int";



  /**
   * Create a SASL context using the specified parameters. A SASL server will be
   * instantiated only for the DIGEST-MD5 mechanism. The GSSAPI mechanism must
   * instantiate the SASL server as the login context in a separate step.
   *
   * @param saslProps
   *          The properties to use in creating the SASL server.
   * @param serverFQDN
   *          The fully qualified domain name to use in creating the SASL
   *          server.
   * @param mechanism
   *          The SASL mechanism name.
   * @param identityMapper
   *          The identity mapper to use in mapping identities.
   * @throws SaslException
   *           If the SASL server can not be instantiated.
   */
  private SASLContext(final HashMap<String, String> saslProps,
      final String serverFQDN, final String mechanism,
      final IdentityMapper<?> identityMapper) throws SaslException
  {
    this.identityMapper = identityMapper;
    this.mechanism = mechanism;
    this.saslProps = saslProps;
    this.serverFQDN = serverFQDN;

    if (mechanism.equals(SASL_MECHANISM_DIGEST_MD5))
    {
      initSASLServer();
    }
  }



  /**
   * Process the specified callback array.
   *
   * @param callbacks
   *          An array of callbacks that need processing.
   * @throws UnsupportedCallbackException
   *           If a callback is not supported.
   */
  @Override
  public void handle(final Callback[] callbacks)
      throws UnsupportedCallbackException
  {
    for (final Callback callback : callbacks)
    {
      if (callback instanceof NameCallback)
      {
        nameCallback((NameCallback) callback);
      }
      else if (callback instanceof PasswordCallback)
      {
        passwordCallback((PasswordCallback) callback);
      }
      else if (callback instanceof RealmCallback)
      {
        realmCallback((RealmCallback) callback);
      }
      else if (callback instanceof AuthorizeCallback)
      {
        authorizeCallback((AuthorizeCallback) callback);
      }
      else
      {
        final LocalizableMessage message = INFO_SASL_UNSUPPORTED_CALLBACK.get(mechanism, callback);
        throw new UnsupportedCallbackException(callback, message.toString());
      }
    }
  }



  /**
   * The method performs all GSSAPI processing. It is run as the context of the
   * login context performed by the GSSAPI mechanism handler. See comments for
   * processing overview.
   *
   * @return {@code true} if the authentication processing was successful.
   */
  @Override
  public Boolean run()
  {
    final ClientConnection clientConn = bindOp.getClientConnection();

    // If the SASL server is null then this is the first handshake and the
    // server needs to be initialized before any processing can be performed.
    // If the SASL server cannot be created then all processing is abandoned
    // and INVALID_CREDENTIALS is returned to the client.
    if (saslServer == null)
    {
      try
      {
        initSASLServer();
      }
      catch (final SaslException ex)
      {
        logger.traceException(ex);
        final GSSException gex = (GSSException) ex.getCause();

        final LocalizableMessage msg;
        if (gex != null)
        {
          msg = ERR_SASL_CONTEXT_CREATE_ERROR.get(SASL_MECHANISM_GSSAPI,
              GSSAPISASLMechanismHandler.getGSSExceptionMessage(gex));
        }
        else
        {
          msg = ERR_SASL_CONTEXT_CREATE_ERROR.get(SASL_MECHANISM_GSSAPI,
              getExceptionMessage(ex));
        }

        clientConn.setSASLAuthStateInfo(null);
        bindOp.setAuthFailureReason(msg);
        bindOp.setResultCode(ResultCode.INVALID_CREDENTIALS);
        return false;
      }
    }

    final ByteString clientCredentials = bindOp.getSASLCredentials();
    clientConn.setSASLAuthStateInfo(null);
    try
    {
      final ByteString responseAuthStr = evaluateResponse(clientCredentials);

      // If the bind has not been completed,then
      // more handshake is needed and SASL_BIND_IN_PROGRESS is returned back
      // to the client.
      if (isBindComplete())
      {
        bindOp.setResultCode(ResultCode.SUCCESS);
        bindOp.setSASLAuthUserEntry(authEntry);
        final AuthenticationInfo authInfo = new AuthenticationInfo(authEntry,
            authzEntry, mechanism, clientCredentials,
            DirectoryServer.isRootDN(authEntry.getName()));
        bindOp.setAuthenticationInfo(authInfo);

        // If confidentiality/integrity has been negotiated then
        // create a SASL security provider and save it in the client
        // connection. If confidentiality/integrity has not been
        // negotiated, dispose of the SASL server.
        if (isConfidentialIntegrity())
        {
          final SASLByteChannel saslByteChannel = SASLByteChannel
              .getSASLByteChannel(clientConn, mechanism, this);
          final LDAPClientConnection ldapConn =
              (LDAPClientConnection) clientConn;
          ldapConn.setSASLPendingProvider(saslByteChannel);
        }
        else
        {
          dispose();
          clientConn.setSASLAuthStateInfo(null);
        }
      }
      else
      {
        bindOp.setServerSASLCredentials(responseAuthStr);
        clientConn.setSASLAuthStateInfo(this);
        bindOp.setResultCode(ResultCode.SASL_BIND_IN_PROGRESS);
      }
    }
    catch (final SaslException e)
    {
      logger.traceException(e);

      final LocalizableMessage msg = ERR_SASL_PROTOCOL_ERROR.get(mechanism,
          getExceptionMessage(e));
      handleError(msg);
      return false;
    }

    return true;
  }



  /**
   * Dispose of the SASL server instance.
   */
  void dispose()
  {
    try
    {
      saslServer.dispose();
    }
    catch (final SaslException e)
    {
      logger.traceException(e);
    }
  }



  /**
   * Evaluate the final stage of a DIGEST-MD5 SASL bind using the specified bind
   * operation.
   *
   * @param bindOp
   *          The bind operation to use in processing.
   */
  void evaluateFinalStage(final BindOperation bindOp)
  {
    this.bindOp = bindOp;
    final ByteString clientCredentials = bindOp.getSASLCredentials();

    if (clientCredentials == null || clientCredentials.length() == 0)
    {
      final LocalizableMessage msg = ERR_SASL_NO_CREDENTIALS.get(mechanism, mechanism);
      handleError(msg);
      return;
    }

    final ClientConnection clientConn = bindOp.getClientConnection();
    clientConn.setSASLAuthStateInfo(null);

    try
    {
      final ByteString responseAuthStr = evaluateResponse(clientCredentials);
      bindOp.setResultCode(ResultCode.SUCCESS);
      bindOp.setServerSASLCredentials(responseAuthStr);
      bindOp.setSASLAuthUserEntry(authEntry);
      final AuthenticationInfo authInfo = new AuthenticationInfo(authEntry,
          authzEntry, mechanism, clientCredentials,
          DirectoryServer.isRootDN(authEntry.getName()));
      bindOp.setAuthenticationInfo(authInfo);

      // If confidentiality/integrity has been negotiated, then create a
      // SASL security provider and save it in the client connection for
      // use in later processing.
      if (isConfidentialIntegrity())
      {
        final LDAPClientConnection2 ldapConn = (LDAPClientConnection2) clientConn;
        ldapConn.enableSASL(saslServer);
      }
      else
      {
        dispose();
        clientConn.setSASLAuthStateInfo(null);
      }
    }
    catch (final SaslException e)
    {
      logger.traceException(e);

      final LocalizableMessage msg = ERR_SASL_PROTOCOL_ERROR.get(mechanism,
          getExceptionMessage(e));
      handleError(msg);
    }
  }



  /**
   * Process the initial stage of a DIGEST-MD5 SASL bind using the specified
   * bind operation.
   *
   * @param bindOp
   *          The bind operation to use in processing.
   */
  void evaluateInitialStage(final BindOperation bindOp)
  {
    this.bindOp = bindOp;
    final ClientConnection clientConn = bindOp.getClientConnection();

    try
    {
      final ByteString challenge = evaluateResponse(ByteString.empty());
      bindOp.setResultCode(ResultCode.SASL_BIND_IN_PROGRESS);
      bindOp.setServerSASLCredentials(challenge);
      clientConn.setSASLAuthStateInfo(this);
    }
    catch (final SaslException e)
    {
      logger.traceException(e);
      final LocalizableMessage msg = ERR_SASL_PROTOCOL_ERROR.get(mechanism,
          getExceptionMessage(e));
      handleError(msg);
    }
  }



  /**
   * Returns the negotiated maximum size of protected data which can be received
   * from the client.
   *
   * @return The negotiated maximum size of protected data which can be received
   *         from the client.
   */
  int getMaxReceiveBufferSize()
  {
    String str = (String) saslServer.getNegotiatedProperty(Sasl.MAX_BUFFER);
    if (str != null)
    {
      try
      {
        return Integer.parseInt(str);
      }
      catch (NumberFormatException e)
      {
        logger.traceException(e);
      }
    }

    // Default buffer size if not specified according to Java SASL
    // documentation.
    return 65536;
  }



  /**
   * Returns the negotiated maximum size of raw data which can be sent to the
   * client.
   *
   * @return The negotiated maximum size of raw data which can be sent to the
   *         client.
   */
  int getMaxRawSendBufferSize()
  {
    String str = (String) saslServer.getNegotiatedProperty(Sasl.RAW_SEND_SIZE);
    if (str != null)
    {
      try
      {
        return Integer.parseInt(str);
      }
      catch (NumberFormatException e)
      {
        logger.traceException(e);
      }
    }

    // Default buffer size if not specified according to Java SASL
    // documentation.
    return 65536;
  }



  /**
   * Return the Security Strength Factor of the cipher if the QOP property is
   * confidentiality, or, 1 if it is integrity.
   *
   * @return The SSF of the cipher used during confidentiality or integrity
   *         processing.
   */
  int getSSF()
  {
    int ssf = 0;
    final String qop = (String) saslServer.getNegotiatedProperty(Sasl.QOP);
    if (integrity.equalsIgnoreCase(qop))
    {
      ssf = 1;
    }
    else if (confidentiality.equalsIgnoreCase(qop))
    {
      final String negStrength = (String) saslServer
          .getNegotiatedProperty(Sasl.STRENGTH);
      if ("low".equalsIgnoreCase(negStrength))
      {
        ssf = 40;
      }
      else if ("medium".equalsIgnoreCase(negStrength))
      {
        ssf = 56;
      }
      else if ("high".equalsIgnoreCase(negStrength))
      {
        ssf = 128;
      }
      /* Treat anything else as if not security is provided and keep the
        server running
       */
    }
    return ssf;
  }



  /**
   * Return {@code true} if the bind has been completed. If the context is
   * supporting confidentiality or integrity, the security provider will need to
   * check if the context has completed the handshake with the client and is
   * ready to process confidentiality or integrity messages.
   *
   * @return {@code true} if the handshaking is complete.
   */
  boolean isBindComplete()
  {
    return saslServer.isComplete();
  }



  /**
   * Perform the authentication as the specified login context. The specified
   * bind operation needs to be saved so the callbacks have access to it. Only
   * used by the GSSAPI mechanism.
   *
   * @param loginContext
   *          The login context to perform the authentication as.
   * @param bindOp
   *          The bind operation needed by the callbacks to process the
   *          authentication.
   */
  void performAuthentication(final LoginContext loginContext,
      final BindOperation bindOp)
  {
    this.bindOp = bindOp;
    try
    {
      Subject.doAs(loginContext.getSubject(), this);
    }
    catch (final PrivilegedActionException e)
    {
      logger.traceException(e);
      final LocalizableMessage msg = ERR_SASL_PROTOCOL_ERROR.get(mechanism,
          getExceptionMessage(e));
      handleError(msg);
    }
  }



  /**
   * Unwrap the specified byte array using the provided offset and length
   * values. Used only when the SASL server has negotiated confidentiality or
   * integrity processing.
   *
   * @param bytes
   *          The byte array to unwrap.
   * @param offset
   *          The offset in the array.
   * @param len
   *          The length from the offset of the number of bytes to unwrap.
   * @return A byte array containing the clear or unwrapped bytes.
   * @throws SaslException
   *           If the bytes cannot be unwrapped.
   */
  byte[] unwrap(final byte[] bytes, final int offset, final int len)
      throws SaslException
  {
    return saslServer.unwrap(bytes, offset, len);
  }



  /**
   * Wrap the specified clear byte array using the provided offset and length
   * values. Used only when the SASL server has negotiated
   * confidentiality/integrity processing.
   *
   * @param clearBytes
   *          The clear byte array to wrap.
   * @param offset
   *          The offset into the clear byte array..
   * @param len
   *          The length from the offset of the number of bytes to wrap.
   * @return A byte array containing the wrapped bytes.
   * @throws SaslException
   *           If the clear bytes cannot be wrapped.
   */
  byte[] wrap(final byte[] clearBytes, final int offset, final int len)
      throws SaslException
  {
    return saslServer.wrap(clearBytes, offset, len);
  }



  /**
   * This callback is used to process the authorize callback. It is used during
   * both GSSAPI and DIGEST-MD5 processing. When processing the GSSAPI
   * mechanism, this is the only callback invoked. When processing the
   * DIGEST-MD5 mechanism, it is the last callback invoked after the name and
   * password callbacks respectively.
   *
   * @param callback
   *          The authorize callback instance to process.
   */
  private void authorizeCallback(final AuthorizeCallback callback)
  {
    final String responseAuthzID = callback.getAuthorizationID();

    // If the authEntry is null, then we are processing a GSSAPI SASL bind,
    // and first need to try to map the authentication ID to an user entry.
    // The authEntry is never null, when processing a DIGEST-MD5 SASL bind.
    if (authEntry == null)
    {
      final String authid = callback.getAuthenticationID();
      try
      {
        authEntry = identityMapper.getEntryForID(authid);
        if (authEntry == null)
        {
          setCallbackMsg(ERR_SASL_AUTHENTRY_NO_MAPPED_ENTRY.get(authid));
          callback.setAuthorized(false);
          return;
        }
      }
      catch (final DirectoryException de)
      {
        logger.traceException(de);
        setCallbackMsg(ERR_SASL_CANNOT_MAP_AUTHENTRY.get(authid,
            de.getMessage()));
        callback.setAuthorized(false);
        return;
      }
      userName = authid;
    }

    if (responseAuthzID.length() == 0)
    {
      setCallbackMsg(ERR_SASLDIGESTMD5_EMPTY_AUTHZID.get());
      callback.setAuthorized(false);
    }
    else if (!responseAuthzID.equals(userName))
    {
      final String lowerAuthzID = toLowerCase(responseAuthzID);

      // Process the callback differently depending on if the authzid
      // string begins with the string "dn:" or not.
      if (lowerAuthzID.startsWith("dn:"))
      {
        authzDNCheck(callback);
      }
      else
      {
        authzIDCheck(callback);
      }
    }
    else
    {
      authzEntry = authEntry;
      callback.setAuthorized(true);
    }
  }



  /**
   * Process the specified authorize callback. This method is called if the
   * callback's authorization ID begins with the string "dn:".
   *
   * @param callback
   *          The authorize callback to process.
   */
  private void authzDNCheck(final AuthorizeCallback callback)
  {
    final String responseAuthzID = callback.getAuthorizationID();
    DN authzDN;
    callback.setAuthorized(true);

    try
    {
      authzDN = DN.valueOf(responseAuthzID.substring(3));
    }
    catch (final LocalizedIllegalArgumentException e)
    {
      logger.traceException(e);
      setCallbackMsg(ERR_SASL_AUTHZID_INVALID_DN.get(responseAuthzID,
          e.getMessageObject()));
      callback.setAuthorized(false);
      return;
    }

    final DN actualAuthzDN = DirectoryServer.getActualRootBindDN(authzDN);
    if (actualAuthzDN != null)
    {
      authzDN = actualAuthzDN;
    }

    if (!authzDN.equals(authEntry.getName()))
    {
      if (authzDN.isRootDN())
      {
        authzEntry = null;
      }
      else
      {
        try
        {
          authzEntry = DirectoryServer.getEntry(authzDN);
          if (authzEntry == null)
          {
            setCallbackMsg(ERR_SASL_AUTHZID_NO_SUCH_ENTRY.get(authzDN));
            callback.setAuthorized(false);
            return;
          }
        }
        catch (final DirectoryException e)
        {
          logger.traceException(e);
          setCallbackMsg(ERR_SASL_AUTHZID_CANNOT_GET_ENTRY.get(authzDN, e.getMessageObject()));
          callback.setAuthorized(false);
          return;
        }
      }
      final AuthenticationInfo authInfo = new AuthenticationInfo(authEntry,
          DirectoryServer.isRootDN(authEntry.getName()));
      if (!hasPrivilege(authInfo))
      {
        callback.setAuthorized(false);
      }
      else
      {
        callback.setAuthorized(hasPermission(authInfo));
      }
    }
  }



  /**
   * Process the specified authorize callback. This method is called if the
   * callback's authorization ID does not begin with the string "dn:".
   *
   * @param callback
   *          The authorize callback to process.
   */
  private void authzIDCheck(final AuthorizeCallback callback)
  {
    final String authzid = callback.getAuthorizationID();
    final String lowerAuthzID = toLowerCase(authzid);
    String idStr;
    callback.setAuthorized(true);

    if (lowerAuthzID.startsWith("u:"))
    {
      idStr = authzid.substring(2);
    }
    else
    {
      idStr = authzid;
    }

    if (idStr.length() == 0)
    {
      authzEntry = null;
    }
    else
    {
      try
      {
        authzEntry = identityMapper.getEntryForID(idStr);
        if (authzEntry == null)
        {
          setCallbackMsg(ERR_SASL_AUTHZID_NO_MAPPED_ENTRY.get(authzid));
          callback.setAuthorized(false);
          return;
        }
      }
      catch (final DirectoryException e)
      {
        logger.traceException(e);
        setCallbackMsg(ERR_SASL_AUTHZID_NO_MAPPED_ENTRY.get(authzid));
        callback.setAuthorized(false);
        return;
      }
    }

    if (authzEntry == null || !authzEntry.getName().equals(authEntry.getName()))
    {
      // Create temporary authorization information and run it both
      // through the privilege and then the access control subsystems.
      final AuthenticationInfo authInfo = new AuthenticationInfo(authEntry,
          DirectoryServer.isRootDN(authEntry.getName()));
      if (!hasPrivilege(authInfo))
      {
        callback.setAuthorized(false);
      }
      else
      {
        callback.setAuthorized(hasPermission(authInfo));
      }
    }
  }



  /**
   * Helper routine to call the SASL server evaluateResponse method with the
   * specified ByteString.
   *
   * @param response A ByteString containing the response to pass to the
   *                 SASL server.
   * @return A ByteString containing the result of the evaluation.
   * @throws SaslException
   *           If the SASL server cannot evaluate the byte array.
   */
  private ByteString evaluateResponse(ByteString response) throws SaslException
  {
    if (response == null)
    {
      response = ByteString.empty();
    }

    final byte[] evalResponse = saslServer.evaluateResponse(response
        .toByteArray());
    if (evalResponse == null)
    {
      return ByteString.empty();
    }
    else
    {
      return ByteString.wrap(evalResponse);
    }
  }



  /**
   * Try to get a entry from the directory using the specified DN. Used only for
   * DIGEST-MD5 SASL mechanism.
   *
   * @param userDN
   *          The DN of the entry to retrieve from the server.
   */
  private void getAuthEntry(final DN userDN)
  {
    try
    {
      authEntry = DirectoryServer.getEntry(userDN);
    }
    catch (final DirectoryException e)
    {
      logger.traceException(e);
      setCallbackMsg(ERR_SASL_CANNOT_GET_ENTRY_BY_DN.get(
          userDN, SASL_MECHANISM_DIGEST_MD5, e.getMessageObject()));
    }
  }



  /**
   * This method is used to process an exception that is thrown during bind
   * processing. It will try to determine if the exception is a result of
   * callback processing, and if it is, will try to use a more informative
   * failure message set by the callback. If the exception is a result of a
   * error during the the SASL server processing, the callback message will be
   * null, and the method will use the specified message parameter as the
   * failure reason. This is a more cryptic exception message hard-coded in the
   * SASL server internals. The method also disposes of the SASL server, clears
   * the authentication state and sets the result code to INVALID_CREDENTIALs
   *
   * @param msg
   *          The message to use if the callback message is not null.
   */
  private void handleError(final LocalizableMessage msg)
  {
    dispose();
    final ClientConnection clientConn = bindOp.getClientConnection();
    clientConn.setSASLAuthStateInfo(null);

    // Check if the callback message is null and use that message if not.
    if (cbResultCode != null)
    {
      bindOp.setResultCode(cbResultCode);
    }
    else
    {
      bindOp.setResultCode(ResultCode.INVALID_CREDENTIALS);
    }

    if (cbMsg != null)
    {
      bindOp.setAuthFailureReason(cbMsg);
    }
    else
    {
      bindOp.setAuthFailureReason(msg);
    }
  }



  /**
   * Checks the specified authentication information parameter against the
   * access control subsystem to see if it has the "proxy" right.
   *
   * @param authInfo
   *          The authentication information to check access on.
   * @return {@code true} if the authentication information has proxy access.
   */
  private boolean hasPermission(final AuthenticationInfo authInfo)
  {
    boolean ret = true;
    Entry e = authzEntry;

    // If the authz entry is null, use the entry associated with the NULL DN.
    if (e == null)
    {
      try
      {
        e = DirectoryServer.getEntry(DN.rootDN());
      }
      catch (final DirectoryException ex)
      {
        return false;
      }
    }

    if (!AccessControlConfigManager.getInstance().getAccessControlHandler()
        .mayProxy(authInfo.getAuthenticationEntry(), e, bindOp))
    {
      setCallbackMsg(ERR_SASL_AUTHZID_INSUFFICIENT_ACCESS.get(authEntry.getName()));
      ret = false;
    }

    return ret;
  }



  /**
   * Checks the specified authentication information parameter against the
   * privilege subsystem to see if it has PROXIED_AUTH privileges.
   *
   * @param authInfo
   *          The authentication information to use in the check.
   * @return {@code true} if the authentication information has PROXIED_AUTH
   *         privileges.
   */
  private boolean hasPrivilege(final AuthenticationInfo authInfo)
  {
    boolean ret = true;
    final InternalClientConnection tempConn = new InternalClientConnection(
        authInfo);
    if (!tempConn.hasPrivilege(Privilege.PROXIED_AUTH, bindOp))
    {
      setCallbackMsg(ERR_SASL_AUTHZID_INSUFFICIENT_PRIVILEGES.get(authEntry.getName()));
      ret = false;
    }
    return ret;
  }



  /**
   * Initialize the SASL server using parameters specified in the constructor.
   */
  private void initSASLServer() throws SaslException
  {
    saslServer = Sasl.createSaslServer(mechanism, SASL_DEFAULT_PROTOCOL,
        serverFQDN, saslProps, this);
    if (saslServer == null)
    {
      final LocalizableMessage msg = ERR_SASL_CREATE_SASL_SERVER_FAILED.get(mechanism,
          serverFQDN);
      throw new SaslException(msg.toString());
    }
  }



  /**
   * Return true if the SASL server has negotiated with the client to support
   * confidentiality or integrity.
   *
   * @return {@code true} if the context supports confidentiality or integrity.
   */
  private boolean isConfidentialIntegrity()
  {
    boolean ret = false;
    final String qop = (String) saslServer.getNegotiatedProperty(Sasl.QOP);
    if (qop.equalsIgnoreCase(confidentiality)
        || qop.equalsIgnoreCase(integrity))
    {
      ret = true;
    }
    return ret;
  }



  /**
   * Process the specified name callback. Used only for DIGEST-MD5 SASL
   * mechanism.
   *
   * @param nameCallback
   *          The name callback to process.
   */
  private void nameCallback(final NameCallback nameCallback)
  {
    userName = nameCallback.getDefaultName();
    final String lowerUserName = toLowerCase(userName);

    // Process the user name differently if it starts with the string "dn:".
    if (lowerUserName.startsWith("dn:"))
    {
      DN userDN;
      try
      {
        userDN = DN.valueOf(userName.substring(3));
      }
      catch (final LocalizedIllegalArgumentException e)
      {
        logger.traceException(e);
        setCallbackMsg(ERR_SASL_CANNOT_DECODE_USERNAME_AS_DN.get(mechanism,
            userName, e.getMessageObject()));
        return;
      }

      if (userDN.isRootDN())
      {
        setCallbackMsg(ERR_SASL_USERNAME_IS_NULL_DN.get(mechanism));
        return;
      }

      final DN rootDN = DirectoryServer.getActualRootBindDN(userDN);
      if (rootDN != null)
      {
        userDN = rootDN;
      }
      getAuthEntry(userDN);
    }
    else
    {
      // The entry name is not a DN, try to map it using the identity
      // mapper.
      String entryID = userName;
      if (lowerUserName.startsWith("u:"))
      {
        if (lowerUserName.equals("u:"))
        {
          setCallbackMsg(ERR_SASL_ZERO_LENGTH_USERNAME
              .get(mechanism, mechanism));
          return;
        }
        entryID = userName.substring(2);
      }
      try
      {
        authEntry = identityMapper.getEntryForID(entryID);
      }
      catch (final DirectoryException e)
      {
        logger.traceException(e);
        setCallbackMsg(ERR_SASLDIGESTMD5_CANNOT_MAP_USERNAME.get(userName, e.getMessageObject()));
      }
    }
    /*
      At this point, the authEntry should not be null.
      If it is, it's an error, but the password callback will catch it.
      There is no way to stop the processing from the name callback.
    */
  }



  /**
   * Process the specified password callback. Used only for the DIGEST-MD5 SASL
   * mechanism. The password callback is processed after the name callback.
   *
   * @param passwordCallback
   *          The password callback to process.
   */
  private void passwordCallback(final PasswordCallback passwordCallback)
  {
    // If there is no authEntry this is an error.
    if (authEntry == null)
    {
      setCallbackMsg(ERR_SASL_NO_MATCHING_ENTRIES.get(userName));
      return;
    }

    // Try to get a clear password to use.
    List<ByteString> clearPasswords;
    try
    {
      final AuthenticationPolicyState authState = AuthenticationPolicyState
          .forUser(authEntry, false);

      if (!authState.isPasswordPolicy())
      {
        final LocalizableMessage message = ERR_SASL_ACCOUNT_NOT_LOCAL.get(mechanism,authEntry.getName());
        setCallbackMsg(ResultCode.INAPPROPRIATE_AUTHENTICATION, message);
        return;
      }

      final PasswordPolicyState pwPolicyState = (PasswordPolicyState) authState;

      clearPasswords = pwPolicyState.getClearPasswords();
      if (clearPasswords == null || clearPasswords.isEmpty())
      {
        setCallbackMsg(ERR_SASL_NO_REVERSIBLE_PASSWORDS.get(mechanism, authEntry.getName()));
        return;
      }
    }
    catch (final Exception e)
    {
      logger.traceException(e);
      setCallbackMsg(ERR_SASL_CANNOT_GET_REVERSIBLE_PASSWORDS.get(authEntry.getName(), mechanism, e));
      return;
    }

    // Use the first password.
    final char[] password = clearPasswords.get(0).toString().toCharArray();
    passwordCallback.setPassword(password);
  }



  /**
   * This callback is used to process realm information. It is not used.
   *
   * @param callback
   *          The realm callback instance to process.
   */
  private void realmCallback(final RealmCallback callback)
  {
  }



  /**
   * Sets the callback message to the specified message.
   *
   * @param cbMsg
   *          The message to set the callback message to.
   */
  private void setCallbackMsg(final LocalizableMessage cbMsg)
  {
    setCallbackMsg(ResultCode.INVALID_CREDENTIALS, cbMsg);
  }



  /**
   * Sets the callback message to the specified message.
   *
   * @param cbResultCode
   *          The result code.
   * @param cbMsg
   *          The message.
   */
  private void setCallbackMsg(final ResultCode cbResultCode,
      final LocalizableMessage cbMsg)
  {
    this.cbResultCode = cbResultCode;
    this.cbMsg = cbMsg;
  }
}
